Skip to content

feat(emitters): add the dotnet facts emitter and centralize config globs - #5

Merged
John-David Dalton (jdalton) merged 4 commits into
mainfrom
dotnet-facts-emitter
Aug 5, 2026
Merged

feat(emitters): add the dotnet facts emitter and centralize config globs#5
John-David Dalton (jdalton) merged 4 commits into
mainfrom
dotnet-facts-emitter

Conversation

@jdalton

@jdalton John-David Dalton (jdalton) commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

.NET/NuGet joins the facts pipeline at parity with the JVM emitters. Original work by Jeppe Fredsgaard Blaabjerg (@jfblaa) in SocketDev/socket-cli#1404; this moves the reusable half into the repo that owns the emitters and the wire contracts, and the CLI command surface stays behind in socket-cli.

Warning

Do not release a version that emits the sidecar ecosystem tag until the reachability sidecar consumer ships its nuget schema change. The scope is wider than it first looks: the producer stamps the tag on every entry, and the consumer parses the component object strictly, so a plain single-ecosystem Gradle or Maven reachability scan fails at the sidecar handoff too. Not just polyglot .NET+JVM ones.

What this adds

A bundled C# tool that runs one MSBuild session per scan — evaluate, restore in-process, then read each project.assets.json through NuGet's own APIs. Doing all three under a single global-property bag is the point: restore and the emitted graph can never end up describing different builds.

runFactsGeneration now accepts tool: 'dotnet', and the resolution report speaks NuGet. It says "target framework" and points at --exclude-target-frameworks, instead of telling a .NET user to pass a JVM option that does not exist.

The C# tool ships no NuGet or MSBuild runtime assemblies — it compiles low and runs high against the SDK it finds

The tool targets net6.0 with RollForward LatestMajor and marks every NuGet reference ExcludeAssets="runtime". At runtime the assemblies come from the locator-selected SDK.

That is deliberate rather than incidental. The in-process restore loads the SDK's own NuGet.Build.Tasks, and if the tool carried its own NuGet copies they would ref/def-clash with the SDK's on any machine whose NuGet version differs — a 0x80131040 failure that looks like a broken feed. Compiling against the oldest supported surface and letting the SDK provide the implementation binds correctly on every SDK from 6 up.

Restore also forces TreatWarningsAsErrors=false, so a project that promotes NuGet warnings to errors cannot abort the run. NU1902/NU1903/NU1904 are security advisories, which a Socket scan should report as findings rather than choke on.

Four fail-closed fixes on top of the original branch — each one is a case where a wrong scan could have exited green

The promise the tool's design leans on is that a wrong scan never exits 0. Four paths broke it.

Problem Why it was silent Fix
The records writer used the platform newline On Windows a stray \r glued to each record's last field flips prod/direct flags, orphans every edge, and fails every artifact path's exists-check NewLine = "\n", and the parser now strips a trailing \r so one emitter regressing cannot corrupt a scan
A crash after partial output wrote no failure record A consumer that treats "crashed" as "records file is empty" publishes a truncated SBOM as a success The catch-all emits a failure record before exiting non-zero
The restore fallback record counted captured errors Errors filtered as noise still sat in the collection, so a failed restore could report nothing, and a leftover project.assets.json then read as a fresh scan Gate on failures reported, not errors caught
packages.config was checked before PackageReference A migrated project with a leftover packages.config got scanned via the flat legacy path, reporting a stale pinned list as its closure Match NuGet's own precedence, and warn when a project has both

The first one has a regression test: a CRLF records stream now has to assemble byte-identically to an LF one.

One glob implementation instead of four — the emitters receive compiled patterns now

--include-configs/--exclude-configs globs compile to anchored regex sources once, in src/run/config-glob.mts, and every emitter is handed the compiled patterns. That deletes the hand-maintained globToRegex from the Gradle init script, the sbt plugin, and the Maven extension, which were three copies of one algorithm with nothing keeping them in sync.

The emitted subset is restricted to what behaves identically in JS RegExp, Java java.util.regex, and .NET Regex, and a vector table pins the semantics all four consumers see.

One intentional behavior change: an emitter that receives a pattern it cannot compile now drops it rather than degrading to a literal match. Since an empty include list means include-everything, a dropped include widens the scan. That is safe because every pattern this package emits is validated before it is sent, and it is documented in the module header for anyone driving a shipped emitter by hand.

Contract changes — an ecosystem tag, per-tool purl types, and per-project config attribution
  • Sidecar entries carry an ecosystem tag. A groupless NuGet id and a Maven artifactId can produce the same coordinate key, so without the tag they merge into one entry. The producer always writes it; a payload without it is still valid and means maven, which is exactly what every sidecar written before the tag looks like. Strict producer, liberal consumer.
  • Components and projects carry the purl type their tool produces, from an exhaustive per-tool map rather than a dotnet ? nuget : maven ternary, so a fifth tool fails to type-check until someone names its type instead of silently assembling maven-typed components.
  • A groupless nuget coordinate omits namespace instead of serializing an empty one. Maven-type entries keep the empty key, which is the shape existing consumers match identity on.
  • Resolution reports carry configsByProject. The flat scannedConfigs union loses attribution as soon as two projects resolve different sets, which is routine for .NET where each project picks its own target frameworks.

docs/agents.md/repo/contract.md records why the ecosystem addition follows the coordinated-release rule rather than escaping it.

Verification

Ran: pnpm test (125 passed), pnpm run type, pnpm run lint --all, pnpm run check --all.

Built and exercised the C# tool. pnpm run build:dotnet-tool compiles it against SDK 10.0.102, and running the built emitter against a scratch project produced an LF-only records file that this package's parser turned into a nuget-typed SBOM and an ecosystem-tagged sidecar passing validateResolvedPathsSidecar.

check --all status: four checks fail, and all four already fail on the default branch — dispatch-table-is-current, prose-em-dash-chains-are-absent, coverage-badge-is-current, and commits-have-no-ai-attribution, which refuses to report a pass on a shallow clone. This branch also clears a fifth that the default branch fails, entry-scripts-are-fail-soft, in its own commit.

Worth a look before release: the build reports NU1903/NU1904 advisories on the pinned NuGet 6.0.0 packages. They are ExcludeAssets="runtime" compile-time references so no vulnerable bytes ship, but the advisory is real.

Left in socket-cli

The command surface — cmd-manifest-dotnet, auto-manifest detection, the setup wizard, socket.json defaults, and the rollup/provenance wiring — stays with the CLI and can land as a small follow-up pinned against a facts release.


Note

High Risk
Large new emitter plus sidecar ecosystem on every entry requires a coordinated consumer schema release before shipping; mis-timed release breaks all strict sidecar parses including single-ecosystem JVM scans.

Overview
Adds .NET/NuGet facts generation at parity with the JVM emitters: runFactsGeneration accepts tool: 'dotnet', resolves the published socket-facts-dotnet assembly via assertDotnetToolBuilt, and runs a single-session MSBuild flow (evaluate → in-process restore → read project.assets.json) that emits the same TSV records protocol. Packaging gains pnpm run build:dotnet-tool, publish-dir paths, and --require-built gating for the dotnet artifact alongside Maven.

Centralizes --include-configs / --exclude-configs semantics in src/run/config-glob.mts: globs compile once to anchored regex sources and are passed pre-compiled to Gradle, sbt, Maven, and the dotnet tool (replacing four duplicate globToRegex implementations).

Extends wire contracts and assembly: NuGet components use purl type nuget (groupless coords omit namespace); resolution reports add configsByProject; sidecar entries always carry ecosystem and accumulate by ecosystem|coord so colliding Maven/NuGet names do not merge. NuGet-specific failure rendering uses target-framework wording and --exclude-target-frameworks. The records parser strips trailing \r for CRLF safety.

Fail-closed hardening in the C# emitter includes LF-only records, catastrophic-crash failure records, restore failure reporting vs filtered noise, and PackageReference-over-packages.config precedence matching NuGet.

Reviewed by Cursor Bugbot for commit 93ea26c. Configure here.

@socket-security

socket-security Bot commented Aug 5, 2026

Copy link
Copy Markdown

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

@socket-security-staging

socket-security-staging Bot commented Aug 5, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
High CVE: Microsoft Security Advisory CVE-2024-38095 | .NET Denial of Service Vulnerability in nuget system.formats.asn1

CVE: GHSA-447r-wph3-92pm Microsoft Security Advisory CVE-2024-38095 | .NET Denial of Service Vulnerability (HIGH)

Affected versions: >= 5.0.0-preview.7.20364.11 < 6.0.1; >= 7.0.0-preview.1.22076.8 < 8.0.1

Patched version: 6.0.1

From: emitters/dotnet-tool/socket-facts-dotnet.csprojnuget/nuget.packaging@6.0.6nuget/system.formats.asn1@5.0.0

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity-Staging ignore nuget/system.formats.asn1@5.0.0. You can also ignore all packages with @SocketSecurity-Staging ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
High CVE: Microsoft Security Advisory CVE-2024-43485 | .NET Denial of Service Vulnerability in nuget system.text.json

CVE: GHSA-8g4q-xg66-9fp4 Microsoft Security Advisory CVE-2024-43485 | .NET Denial of Service Vulnerability (HIGH)

Affected versions: >= 8.0.0 < 8.0.5; >= 6.0.0 < 6.0.10

Patched version: 6.0.10

From: emitters/dotnet-tool/socket-facts-dotnet.csprojnuget/microsoft.build@17.3.2nuget/system.text.json@6.0.0

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity-Staging ignore nuget/system.text.json@6.0.0. You can also ignore all packages with @SocketSecurity-Staging ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 93ea26c. Configure here.

)
return await assembleFromRecords(out, recordsFile)
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dotnet path ignores excludePaths

Medium Severity

runDotnet never forwards excludePaths, and the C# tool has no exclude-path option. Callers that rely on the shared FactsGenerationOptions.excludePaths contract get a full .NET project graph anyway, unlike Gradle, Maven, and sbt.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 93ea26c. Configure here.

Bring .NET/NuGet to the facts pipeline at parity with the JVM emitters.

Emitter:
- Bundled C# tool (socket-facts-dotnet) running one MSBuild session —
  evaluate, in-process restore, read project.assets.json through
  NuGet.ProjectModel — emitting the shared records TSV. Ships no NuGet or
  MSBuild runtime assemblies: it compiles low and runs high against the
  locator-selected SDK (net6 floor, RollForward LatestMajor), so it works
  across installed SDK versions and avoids the ref/def assembly clash that
  breaks multi-SDK hosts. Restore forces TreatWarningsAsErrors=false so
  NU19xx security advisories do not abort the run. packages.config is
  supported: flat pinned closure, developmentDependency to dev, HintPath
  DLLs, and pinned-artifact download through the project's configured
  feeds. Fail-closed: it records failures instead of throwing, and every
  emitted artifact path is guaranteed to exist.
- runFactsGeneration accepts tool: 'dotnet'; assets.mts resolves the
  published tool and throws when a packaging defect leaves it out.
- NuGet resolution dialect, including the noun a .NET user expects: the
  report says "target framework" and points at
  --exclude-target-frameworks rather than --exclude-configs.

Contract:
- Components and projects carry the purl type their tool produces, from an
  exhaustive per-tool map, and a groupless nuget coordinate omits the
  namespace key instead of serializing an empty one.
- Sidecar entries carry an ecosystem tag, so a nuget coordinate and a maven
  coordinate that share a name stay distinct. The producer always writes
  it; a payload without it is still valid and means maven. Shipping this
  needs the reachability sidecar's nuget schema change released first,
  because that consumer parses strictly. See docs/agents.md/repo/contract.md.
- Resolution reports carry configsByProject, which attributes each resolved
  configuration to the project that resolved it. The flat union loses that
  as soon as two projects resolve different sets, which is routine for .NET.

Config-name globs:
- Compile include/exclude globs to anchored regex sources once, in
  config-glob.mts, and hand every emitter the compiled patterns. Removes
  the per-language globToRegex from the Gradle, sbt, and Maven emitters and
  adds a cross-language vector suite.

Fail-closed fixes over the original branch:
- The records writer forces LF. On Windows the default would glue a stray
  carriage return to each record's last field, flipping prod/direct flags,
  orphaning every edge, and failing every artifact path's exists-check,
  with the scan still reporting success. The records parser now also
  tolerates CRLF, so one emitter regressing cannot corrupt a scan.
- A crash after partial output records a failure, so a truncated SBOM can
  no longer be published as a success.
- The restore fallback record is gated on what was reported rather than on
  what a logger captured, so a restore whose every error was filtered as
  noise can no longer report a stale project.assets.json as a fresh scan.
- PackageReference is checked before packages.config, matching NuGet's own
  precedence, so a leftover packages.config from a migration no longer wins
  over the project's real dependency graph. A project carrying both gets a
  warning.
Importing either build script as a library ran main() against the caller's
argv. The dotnet build script lands with the guard already in place; these
two are its siblings, and entry-scripts-are-fail-soft flags all three
together.
Splitting artifact-paths out of assemble left the type import behind. The
declaration build runs with noUnusedLocals, so it failed there while the
type check passed.
…6.0.6

6.0.0 carries GHSA-68w7-72jg-6qpp, a critical NuGet client security-feature
bypass, plus two high-severity advisories on NuGet.Common and NuGet.Protocol.
Socket's own scanner blocks the PR on it.

6.0.6 is the patched floor of the same line, so the compile-low/run-high
guarantee the references exist for is unchanged: the tool still binds on every
SDK from 6 up and still ships no NuGet runtime assets.
@jdalton
John-David Dalton (jdalton) merged commit d7eddc3 into main Aug 5, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants